Mixed Render Architecture

0x1 混合渲染架构

在开发渲染程序的过程中,我们常常会碰到需要把多个渲染引擎混合在一起使用的场景,如android中渲染视频数据的时候还需要渲染UI。游戏中渲染游戏3D场景的时候还需要渲染UI等场景。本文对这种混合渲染的架构进行了总结。下面对五种混合渲染方式进行了分别介绍。

0x2 SurfaceView


我们知道SurfaceView是在Android中提出的概念。Android中的UI显示采用了View的架构进行处理,View可以满足大部分的绘图需求,但是有时候,View却显得力不从心,所以Android提供了SurfaceView给Android开发者,以满足更多的绘图需求。

SurfaceView是一个组件,可用于在View层次结构中嵌入其他合成层。但因为SurfaceView拥有独立于主View之外的独立渲染线程,SurfaceView的内容对其他View来说是透明的,也就是说SurfaceView的绘制游离于其他View的控制之外。

使用SurfaceView进行渲染时,SurfaceFlinger会直接将SurfaceView的输出缓冲区直接合成到屏幕上。如果没有SurfaceView,则需要将绘制内容先绘制到其他View对应的Surface中,注意该Surface也包括了其他View的内容绘制,然后再作为一层合成到屏幕上,而使用SurfaceView进行渲染可以省去额外的工作。但是SurfaceView是作为单独的一层合成到屏幕上的,在拥有绘制灵活性的同时也消耗了更多的内存。

View和SurfaceView的区别主要有这两点,
一是View适用于主动更新的情况,而SurfaceView则适用于被动更新的情况,比如频繁刷新界面。另外一点是View是在主线程中对页面进行刷新,而SurfaceView则需要开启一个子线程来对页面进行刷新。

GLSurfaceView
GLSurfaceView提供了帮助管理EGL上下文、在线程间通信以及与activity生命周期交互的辅助程序类。GLSurfaceView会创建一个渲染线程,并在线程上配置EGL上下文。大多数应用无需了解有关 EGL 的任何信息即可通过 GLSurfaceView 来使用GLES。

0x3 TextureView


TextureView一般和SurfaceTexture配合使用。
SurfaceTexture一般包括producer和consumer模块,producer负责生产texture,生产好了通知consumer,consumer把texture作为view的贴图输入完成SurfaceTexture的绘制。TextureView 类是一个结合了 View 和 SurfaceTexture 的 View 对象。可以理解为在主窗口的体系之内的子View,和主窗口的其他View形成父子关系,可以一起联动,形成复杂的渲染效果。

SurfaceTexture 是Surface和OpenGL ES (GLES)纹理的组合。SurfaceTexture实例用于提供输出到 GLES 纹理的接口。

SurfaceTexture包含一个以应用为使用方的BufferQueue实例。当生产方将新的缓冲区排入队列时,onFrameAvailable() 回调会通知应用。然后,应用调用updateTexImage()。这会释放先前占用的缓冲区,从队列中获取新缓冲区并执行EGL调用,从而使GLES可将此缓冲区作为外部纹理使用。

TextureView对象会对SurfaceTexture进行包装,从而响应回调以及获取新的缓冲区。在TextureView获取新的缓冲区时,TextureView会发出View失效请求,并使用最新缓冲区的内容作为数据源进行绘图,根据View状态的指示,以相应的方式在相应的位置进行呈现。

与SurfaceView 相比,TextureView具有更出色的UI绘制操作能力,但在视频上以分层方式合成界面元素时,SurfaceView具有性能方面的优势。当客户端使用SurfaceView呈现内容时,SurfaceView会为客户端提供单独的合成层。如果设备支持,SurfaceFlinger会将单独的层合成为硬件叠加层。当客户端使用TextureView呈现内容时,界面工具包会使用GPU将 TextureView的内容合成到视图层次结构中。对内容进行的更新可能会导致其他View元素重绘,例如,在其他View被置于TextureView顶部时。View呈现完成后,SurfaceFlinger会合成应用界面层和所有其他层,以便每个可见像素合成两次。

0x4 同一个Context 直接渲染


我们把这种渲染混合方式叫做DirectRender。
简单把流程说明如下,这里假设渲染采用EGL与系统窗口系统进行对接。
Main Render负责创建EGL环境,然后CPU准备绘制数据,并把绘制数据送到GPU,然后调用GPU Draw函数进行绘制,注意这个时候并没有调用SwapBuffer,
而是去调用了Sub Render的绘制流程,在SubRender中同样完成绘制数据准备和调用GPU Draw函数进行绘制的过程,最后再去调用SwapBuffer。
在Main Render和Sub Render的切换过程中,涉及到GL状态的切换,可能两个Render之间的设置存在冲突,需要有一个保存和恢复的过程,另外有些状态的设置需要在两个引擎之间配合好,否则会出现各种显示问题。
另外一点是这个Main Render和Sub Render是在同一个渲染线程中运行,没有多线程的切换。

0x5 同一个Context fbo渲染


简单把流程说明如下,
Main Render负责创建EGL环境,然后CPU准备绘制数据,并把绘制数据送到GPU,然后调用GPU Draw函数进行绘制,
然后调用了Sub Render的绘制流程,Sub Render的绘制结果是保存在framebuffer object中,这个framebuffer object和一个texture绑定在一起。
在SubRender中同样完成绘制数据准备和调用GPU Draw函数进行绘制以后,也就是说对framebuffer object进行了写入,这个时候texture也就准备好了,
Main Render拿着这个texture作为输入,再进行一次绘制,最后再去调用SwapBuffer输出。
在Main Render和Sub Render的切换过程中,也涉及到GL状态的切换,有些状态的设置也是需要在两个引擎之间配合好,否则会出现各种显示问题。另外一点是这个Main Render和Sub Render也是在同一个渲染线程中运行。

0x6 Shared context渲染

简单把流程说明如下,
Main Render负责创建EGL环境,然后CPU准备绘制数据,并把绘制数据送到GPU,然后调用GPU Draw函数进行绘制。
Sub Render通过shared context的方式创建,也是需要完成EGL设置的过程,另外这个Sub Render创建的Surface是Pbuffer,是offscreen的。
另外这个Sub Render的绘制结果也是保存在framebuffer object中,这个framebuffer object和一个texture绑定在一起。
在SubRender中同样完成绘制数据准备和调用GPU Draw函数进行绘制以后,这个时候Main Render和Sub Render有一个sync的操作。
保证对Sub Render对framebuffer object进行了写入,texture也是准备好了。
然后Main Render拿着这个texture作为输入,再进行一次绘制,最后再去调用SwapBuffer输出。
在Main Render和Sub Render的切换过程中,由于采用的是Shared Context架构,GL状态在Main Render和Sub Render之间相对比较独立,相互的影响比较小。另外一点是这个Main Render和Sub Render是在两个独立的渲染线程中运行。

下面再对Shared Context的绘制渲染架构进行详细的分析。
我们知道OpenGL是一个线程级别的状态机,其操作被局限在一个线程中进行。OpenGL 的绘制命令都是作用在当前的 Context 上,这个 Current Context是一个线程私有thread-local)的变量。如果在多线程环境下操作同一个opengl context,对opengl api的调用需要加锁进行保护。否则会出现各种问题。
通过共享context架构,上下文是可以在多个线程间共享的,在使用eglCreateContext时, 可以传入一个已创建成功的上下文, 这样就可以得到一个共享的上下文(Shared Context),我们可以实现多个渲染线程并行处理,其中一些线程作为生产者,这些生产者一般是离屏渲染,生成相关纹理供另外的线程消费、

举例来说, Google为Android的MediaCodec设计了一套基于OpenGL的Pipeline, 其涉及的模块包括 Video Capture, Pre-processing, Encode, Decode, Post processing,Render等。上述的整个Pipeline会在多个线程中工作. 可以使用Shared Context的方式为这个架构设计高度优化的Pipeline,这样设计思路也比较自然。如果强制设计为单独context的架构,各个线程的OpenGL操作都需要post任务到OpenGL线程来处理,涉及到多次加锁保护的过程,无论是架构的可读性,还是系统的性能应该都会受到影响。

另外Shared Context架构下共享资源有纹理,shader,Buffer等。不共享资源有FBO, VAO等。

0x7 Shared Contxt架构下纹理共享访问问题

这个问题是Shared Contxt架构下渲染绘制crash问题,场景是webview播放视频的时候crash,crash的位置在GPU驱动中,但是具体原因未知。
大概的渲染流程是这样的,UI渲染是Main Render负责的,webview播放视频是Sub Render绘制完成的,其中用到了Shared context架构,webview播放视频的过程渲染到纹理中,Main Render拿到这个纹理后完成真正的视频内容绘制。
通过分析大概的渲染流程,我怀疑是Shared context架构下纹理访问冲突问题。根据前面的分析,Sub Render会通过填充纹理内容的方式生产绘制内容,Main Render得到纹理内容以后再读入GPU,最后完成渲染。如果Main Render读取纹理内容的同时,Sub Render正在填充纹理的话,肯定就发生访问冲突了。

我们通过一个单元测试来模拟这个问题。看看出现crash的地方是否和真实场景下出错的地方一致。
piglit下面有相关的Shared Context测试,代码位于piglit/tests/glx/glx-multithread-texture.c
其中包括一个纹理填充线程,一个纹理读取线程。我们通过在修改测试代码,模拟前面说的读取个填充纹理同时发生的情况。
但是很遗憾,没有出现crash。看来这个问题对timing很敏感,应该是特定时序下发生的问题
看来这能在GPU驱动代码mesa中hack来复现了。如下所示,这个是在Main Render执行纹理读取设置的时候加一个sleep等待。等待Sub Render有足够的时间去修改纹理设置。果然这样hack以后问题必现,而且出错的堆栈和真实场景下完全一致。
这样问题基本定位,最后发送确实是webview里面播放视频的时候分配的buffer不够用,导致Main Render和Sub Render出现纹理访问冲突的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
diff --git a/src/mesa/state_tracker/st_sampler_view.c b/src/mesa/state_tracker/st_sampler_view.c
index e4add1bd2c6..61b3ffe2e33 100644
--- a/src/mesa/state_tracker/st_sampler_view.c
+++ b/src/mesa/state_tracker/st_sampler_view.c
@@ -642,7 +642,15 @@ st_get_texture_sampler_view_from_stobj(struct st_context *st,
...
struct pipe_sampler_view *view = sv->view;
+
+ static int counter = 0;
+ while (counter++ < 20) {
+ sleep(5);
+ }
+
assert(stObj->pt == view->texture);
assert(!check_sampler_swizzle(st, stObj, view, glsl130_or_later));
assert(get_sampler_view_format(st, stObj, srgb_skip_decode) == view->format);

0x8 参考资料

https://source.android.google.cn/devices/graphics/arch-st?hl=zh-cn
https://zhuanlan.zhihu.com/p/444440326